<?php

declare(strict_types=1);

namespace Tools\lock\Redis;

use Tools\lock\Lock;

/**
 * Class Lua
 *
 * @package Tools\lock\Redis
 */
class Lua implements Lock
{
    /**
     * Set lock and set expire command
     */
    const LOCK_COMMAND_EXPIRE = <<<'command'
if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then
	if redis.call("EXPIRE", KEYS[1], ARGV[2]) == 1 then
		return 1
	else
	    if redis.call("DEL", KEYS[1]) == 1 then
	        return -1
	    else
	        return -3
	    end
	end
else
	return -2
end
return 0
command;

    /**
     * Set lock command
     */
    const LOCK_COMMAND = <<<'command'
if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then
	return 1
else
	return -2
end
return 0
command;

    /**
     * COMMAND key number
     */
    const lOCK_COMMAND_KEY_NUMBER = 1;

    /**
     * Remove current lock
     */
    const UNLOCK_COMMAND = <<<'command'
if redis.call("GET", KEYS[1]) == ARGV[1] then
	if redis.call("DEL", KEYS[1]) == 1 then
		return 1
	else
	    return -1
	end
else
	return -2
end
return 0
command;

    /**
     * Unlock command key number
     */
    const UNlOCK_COMMAND_KEY_NUMBER = 1;

    const LOCK_SUCCESS = 1;
    const LOCK_FAILURE = 0;

    const LOCK_EXISTS = -2;
    const LOCK_SET_EXPIRE_FAILURE = -1;
    const LOCK_ADD_EXCEPTION = -3;

    const LOCK_DELETE_FAILURE = -1;
    const LOCK_NOT_CURRENT = -1;

    /**
     * @var ?\Redis $client Connection
     */
    private $client = null;

    /**
     * @var string $key
     */
    private $key;

    /**
     * @var mixed $value
     */
    private $value;

    /**
     * @var int $ttl
     */
    private $ttl;

    /**
     * @var ?array $data
     */
    private $data = null;

    /**
     * @var bool $isLock
     */
    private $isLock = false;

    /**
     * Lua constructor.
     *
     * @param ?\Redis $connection
     */
    public function __construct($connection = null)
    {
        if (is_null($connection)) {
            $this->client = \Illuminate\Support\Facades\Redis::connection()->client();
        } else {
            $this->client = $connection;
        }
    }

    /**
     * Add lock
     *
     * @param string $key
     * @param mixed $value 0
     * @param int $ttl 0
     * @param array $data []
     * @return bool
     */
    public function lock($key, $value = 0, $ttl = 0, $data = [])
    {
        if ($value === 0) $value = bcmul(microtime(true), 1000);

        if ($ttl === 0) {
            $result = $this->client->eval(static::LOCK_COMMAND, [$key, $value], static::UNlOCK_COMMAND_KEY_NUMBER);
        } else {
            $result = $this->client->eval(static::LOCK_COMMAND_EXPIRE, [$key, $value, $ttl], static::UNlOCK_COMMAND_KEY_NUMBER);
        }

        if ($result === static::LOCK_SUCCESS) {
            $this->key = $key;
            $this->value = $value;
            $this->ttl = $ttl;
            $this->data = $data;

            $this->isLock = true;

            return true;
        }

        return false;
    }

    /**
     * Remove lock
     *
     * @return bool|mixed
     */
    public function unlock()
    {
        if (!$this->isLock) return true;

        $result = $this->client->eval(static::UNLOCK_COMMAND, [$this->key, $this->value], static::lOCK_COMMAND_KEY_NUMBER);

        if ($result === static::LOCK_SUCCESS) {
            $this->isLock = false;

            return false;
        }

        return false;
    }

    /**
     * Check that you have a lock
     *
     * @return bool
     */
    public function isLock()
    {
        return $this->isLock;
    }

    /**
     * Check for the exclusive lock
     *
     * @return bool
     */
    public function monopolize()
    {
        if ($this->isLock) {
            $value = $this->client->get($this->key);

            return ($value == $this->value);
        }

        return false;
    }
}
